package org.anderground.app.drawer;

import static org.anderground.core.Logger.logBegin;
import static org.anderground.core.Logger.logEnd;

import java.util.Observable;
import java.util.Observer;

import org.anderground.app.model.AppModel;
import org.anderground.core.Logger;
import org.anderground.core.drawer.Calculator;
import org.anderground.core.drawer.MetroDrawer;
import org.anderground.core.drawer.Property;
import org.anderground.core.drawer.Calculator.Mode;
import org.anderground.core.graphics.Canvas;
import org.anderground.core.graphics.Paint;
import org.anderground.core.graphics.Point;
import org.anderground.core.graphics.PointF;
import org.anderground.core.graphics.Rect;
import org.anderground.core.model.ModelData;
import org.anderground.core.model.Station;
import org.anderground.core.model.Transition;
import org.anderground.internal.graphics.impl.AndroidCanvas;
import org.anderground.modules.drawer.dynamic.DynamicDrawer;

import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;

/**
 * Respond for drawing the map, managing the cache and so on.
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class DrawerProxy extends Drawable implements MetroDrawer {

	private static final MetroDrawer VOID_DRAWER = new VoidDrawer();

	private static Paint paint = new Paint();
	static {
		paint.setARGB(255, 0, 0, 0);
		paint.setStrokeWidth(1);
		paint.setStrokeCap(Paint.Cap.ROUND);
	}

	/** Singleton instance */
	private static DrawerProxy instance = new DrawerProxy();

	/** cache manager */
	private Cache mainCache = new MainDrawCache(),
			selectionCache = new SelectionCache();

	/** the drawer */
	private MetroDrawer drawer = VOID_DRAWER;

	private Station[] selectedStations;
	private Transition[] selectedTransitions;

	{
		AppModel.getInstance().addObserver(new Observer() {
			@Override
			public void update(Observable observable, Object data) {
				prepare(AppModel.getInstance().getData());
			}
		});
	}

	/**
	 * private constructor
	 */
	private DrawerProxy() {
		init(10, 10);
		try {
			setDrawer(DynamicDrawer.class.newInstance());
		} catch (Exception e) {
			Logger.e(e);
		}

	}

	/**
	 * @return the instance
	 */
	public static DrawerProxy getInstance() {
		return instance;
	}

	/**
	 * all class initializations
	 * 
	 * @param width
	 * @param height
	 */
	private void init(int width, int height) {
		mainCache.setSize(width, height);
		selectionCache.setSize(width, height);
	}

	/**
	 * mark cache as invalid
	 */
	public void invalidate() {
		mainCache.invalidate();
		selectionCache.invalidate();
		invalidateSelf();
	}

	@Override
	protected void onBoundsChange(android.graphics.Rect bounds) {
		int width = bounds.width();
		int height = bounds.height();
		if (width != 0 && height != 0) {
			init(width, height);
		}
		super.onBoundsChange(bounds);
	}

	/**
	 * Sets a new delegated drawer, possibly preparing it with current
	 * {@link ModelData} (if it is not null)
	 * 
	 * @param drawer
	 */
	public void setDrawer(MetroDrawer drawer) {
		if (drawer == null) {
			drawer = VOID_DRAWER;
		}

		// Debug.startMethodTracing();
		logBegin("preparing the drawer to work with new model");

		if (drawer.prepare(getModelData(), getMode())) {
			this.drawer = drawer;
			invalidate();
		}

		logEnd("prepare View");
		// Debug.stopMethodTracing();
	}

	public MetroDrawer getDrawer() {
		return drawer;
	}

	/**
	 * @param modelData
	 * @param mode
	 * @return
	 * @see MetroDrawer#prepare(org.anderground.core.model.ModelData,
	 *      Calculator.Mode)
	 */
	@Override
	public boolean prepare(ModelData modelData, Mode mode) {
		Logger.log("preparing drawer");
		boolean b = drawer.prepare(modelData, mode);
		invalidate();
		return b;
	}

	/**
	 * @param modelData
	 * @return
	 */
	public boolean prepare(ModelData modelData) {
		return prepare(modelData, getMode() == null ? Mode.LOCATION : getMode());
	}

	/**
	 * @param mode
	 * @return
	 */
	public boolean prepare(Mode mode) {
		return prepare(getModelData(), mode);
	}

	private final Cache.RedrawCallback redrawCallback = new Cache.RedrawCallback() {
		@Override
		public void onCacheRedrawn() {
			invalidateSelf();
		}
	};

	/**
	 * @param calculator
	 *            the calculator to use for Coord conversion
	 */
	@Override
	public void applyCalculator(Calculator calculator) {
		boolean hasChanged = mainCache.apply(drawer, calculator, false,
				redrawCallback);
		if (hasChanged) {
			delayApplyCalculator(calculator);
			selectionCache.apply(drawer, calculator, false, redrawCallback);
		}
	}

	private Calculator delayedCalculator;

	private void delayApplyCalculator(Calculator calculator) {
		delayedCalculator = calculator;
	}

	private void evaluateApplyCalculator() {
		if (delayedCalculator != null) {
			Logger.d("apply calculator");
			drawer.applyCalculator(delayedCalculator);
			delayedCalculator = null;
		}
	}

	/**
	 * Draw the image on the canvas applying the calculator
	 * 
	 * @param canvas
	 *            the canvas (anderground graphics) to draw the image on
	 * @see MetroDrawer#draw(Canvas)
	 */
	@Override
	public void draw(Canvas canvas) {
		this.draw(AndroidCanvas.cast(canvas));
	}

	/**
	 * Draw the image on the canvas applying the calculator
	 * 
	 * @param canvas
	 *            the canvas (android graphics) to draw the image on
	 */
	@Override
	public void draw(android.graphics.Canvas canvas) {
		// Logger.d("draw!");
		mainCache.draw(canvas);
		selectionCache.draw(canvas);
	}

	/**
	 * @param rect
	 * @see MetroDrawer#copyCoordBounds(Rect)
	 */
	@Override
	public void copyCoordBounds(Rect rect) {
		drawer.copyCoordBounds(rect);
	}

	/**
	 * @return
	 * @see MetroDrawer#getName()
	 */
	@Override
	public String getName() {
		return drawer.getName();
	}

	/**
	 * @return
	 * @see MetroDrawer#getProperties()
	 */
	@Override
	public Property[] getProperties() {
		return drawer.getProperties();
	}

	/**
	 * @return
	 * @see MetroDrawer#isReady()
	 */
	@Override
	public boolean isReady() {
		return drawer.isReady();
	}

	/**
	 * @param point
	 * @return
	 * @see MetroDrawer#searchStations(Point)
	 */
	@Override
	public Station[] searchStations(Point point) {
		Logger.d("search stations");
		evaluateApplyCalculator();
		return drawer.searchStations(point);
	}

	/**
	 * @param canvas
	 * @param stations
	 * @see MetroDrawer#selectStations(Canvas, Station[])
	 */
	@Override
	public void selectStations(Canvas canvas, Station... stations) {
		this.selectedStations = stations;
		selectionCache.draw(AndroidCanvas.cast(canvas));
	}

	/**
	 * @param canvas
	 * @param transitions
	 * @see MetroDrawer#selectTransitions(Canvas, Transition[])
	 */
	@Override
	public void selectTransitions(Canvas canvas, Transition... transitions) {
		this.selectedTransitions = transitions;
		selectionCache.draw(AndroidCanvas.cast(canvas));
	}

	/**
	 * @param properties
	 * @see MetroDrawer#setProperties(Property[])
	 */
	@Override
	public void setProperties(Property[] properties) {
		drawer.setProperties(properties);
	}

	/**
	 * @return
	 * @see MetroDrawer#getMode()
	 */
	@Override
	public Mode getMode() {
		return drawer.getMode();
	}

	/**
	 * @return
	 * @see MetroDrawer#getModelData()
	 */
	@Override
	public ModelData getModelData() {
		return drawer.getModelData();
	}

	@Override
	public int getOpacity() {
		return PixelFormat.TRANSLUCENT;
	}

	@Override
	public void setAlpha(int alpha) {
		// noop
	}

	@Override
	public void setColorFilter(ColorFilter cf) {
		// noop
	}

	/**
	 * @author Eldar Abusalimov
	 */
	private static class VoidDrawer implements MetroDrawer {

		private static final String NAME = "Void drawer";
		private static final Property[] PROPERTIES = new Property[] {};
		private static final Station[] STATIONS = new Station[] {};
		private ModelData modelData = new ModelData();
		private Mode mode = Mode.LOCATION;

		@Override
		public void draw(Canvas canvas) {
		}

		@Override
		public void applyCalculator(Calculator calculator) {
		}

		@Override
		public void copyCoordBounds(Rect rect) {
		}

		@Override
		public Mode getMode() {
			return mode;
		}

		@Override
		public ModelData getModelData() {
			return modelData;
		}

		@Override
		public String getName() {
			return NAME;
		}

		@Override
		public Property[] getProperties() {
			return PROPERTIES;
		}

		@Override
		public boolean isReady() {
			return true;
		}

		@Override
		public boolean prepare(ModelData modelData, Mode mode) {
			this.modelData = modelData == null ? this.modelData : modelData;
			this.mode = mode == null ? this.mode : mode;
			return true;
		}

		@Override
		public Station[] searchStations(Point point) {
			return STATIONS;
		}

		@Override
		public void selectStations(Canvas canvas, Station... stations) {
		}

		@Override
		public void selectTransitions(Canvas canvas, Transition... transitions) {
		}

		@Override
		public void setProperties(Property[] properties) {
		}

	}

	/**
	 * @author Eldar Abusalimov
	 */
	private abstract static class Cache {

		/**
		 * @author Eldar Abusalimov
		 */
		public interface RedrawCallback {

			void onCacheRedrawn();

		}

		public static final int DEFAULT_MULTIPLIER = 3;

		/** min idle time to start cache redrawing */
		private static final int REDRAW_DELAY = 700;

		private boolean valid = false;

		/** in optimization purposes */
		private final PointF oldCoord = new PointF();

		private final PointF minCoordF = new PointF(),
				maxCoordF = new PointF();

		/** view dimensions */
		private final PointF min = new PointF(), max = new PointF(),
				size = new PointF();

		private final PointF newMinCoordF = new PointF(),
				newMaxCoordF = new PointF();

		private final PointF newMinF = new PointF(), newMaxF = new PointF();

		/** cache dimensions multiplier */
		private int dim = DEFAULT_MULTIPLIER;

		/** cache bitmap */
		private Bitmap bitmap;

		/** wrapped canvas to pass to the drawer */
		private org.anderground.core.graphics.Canvas canvas;

		/** current thread handler */
		private Handler handler = new Handler();

		/** this will redraw the map only after a bit delay */
		private final RedrawRunnable redrawRunnable = new RedrawRunnable();

		private RedrawCallback callback;

		/**
		 * default constructor
		 */
		public Cache() {
			this(1, 1, DEFAULT_MULTIPLIER);
		}

		/**
		 * 
		 * @param width
		 * @param height
		 */
		public Cache(int width, int height) {
			this(width, height, DEFAULT_MULTIPLIER);
		}

		/**
		 * 
		 * @param width
		 * @param height
		 * @param dimensionsMultiplier
		 */
		public Cache(int width, int height, int dimensionsMultiplier) {
			init(width, height, dimensionsMultiplier);
		}

		/**
		 * 
		 * @param width
		 * @param height
		 */
		public void setSize(int width, int height) {
			setSize(width, height, dim);
		}

		/**
		 * 
		 * @param width
		 * @param height
		 * @param dimensionsMultiplier
		 */
		public void setSize(int width, int height, int dimensionsMultiplier) {
			if (size.x == width && size.y == height
					&& dimensionsMultiplier == dim) {
				return;
			}
			bitmap.recycle();
			init(width, height, dimensionsMultiplier);
		}

		/**
		 * 
		 * @param width
		 * @param height
		 * @param dimensionsMultiplier
		 */
		private void init(int width, int height, int dimensionsMultiplier) {
			size.set(width, height);
			dim = dimensionsMultiplier;

			int cacheWidth = width * dim;
			int cacheHeight = height * dim;

			min.set((width - cacheWidth) / 2f, (height - cacheHeight) / 2f);
			max.set((width + cacheWidth) / 2f, (height + cacheHeight) / 2f);

			this.bitmap = Bitmap.createBitmap((int) cacheWidth,
					(int) cacheHeight, Bitmap.Config.ARGB_4444);

			android.graphics.Canvas canvas = new android.graphics.Canvas(bitmap);
			canvas.translate((cacheWidth - width) / 2,
					(cacheHeight - height) / 2);

			this.canvas = AndroidCanvas.wrap(canvas);
		}

		/**
		 * @param drawer
		 * @param calculator
		 * @param forceInstantRedraw
		 *            if true then the cache will be updated immediately
		 * @param callback
		 * @return true if new calculator converts points in another way then
		 *         previously applied one, false otherwise
		 */
		public boolean apply(MetroDrawer drawer, Calculator calculator,
				boolean forceInstantRedraw, RedrawCallback callback) {
			boolean hasChanged = !valid;
			this.callback = callback;

			PointF oldCoord = this.oldCoord;
			oldCoord.set(newMinCoordF);
			calculator.revert(min, newMinCoordF);
			if (!hasChanged && !newMinCoordF.equals(oldCoord)) {
				hasChanged = true;
			}
			oldCoord.set(newMaxCoordF);
			calculator.revert(max, newMaxCoordF);
			if (!hasChanged && !newMaxCoordF.equals(oldCoord)) {
				hasChanged = true;
			}

			calculator.convert(minCoordF, newMinF);
			calculator.convert(maxCoordF, newMaxF);
			if (hasChanged || hasChanged()) {
				postRedraw(drawer, calculator, forceInstantRedraw);
			}
			valid = true;

			return hasChanged;
		}

		/**
		 * @param drawer
		 * @param calculator
		 * @param forceInstantRedraw
		 */
		private void postRedraw(MetroDrawer drawer, Calculator calculator,
				boolean forceInstantRedraw) {
			if (forceInstantRedraw) {
				forcePendingRedraw();
			} else {
				redrawRunnable.calculator = calculator;
				redrawRunnable.drawer = drawer;
				handler.removeCallbacks(redrawRunnable);
				handler.postDelayed(redrawRunnable, REDRAW_DELAY);
			}
		}

		/**
		 * Force cache to be redrawn removing any pending redraws that have been
		 * posted to the handler.
		 */
		private void forcePendingRedraw() {
			RedrawRunnable r = redrawRunnable;
			handler.removeCallbacks(r);
			redraw(r.drawer, r.calculator);
		}

		/**
		 * redraw the cached image
		 * 
		 * @param drawer
		 * @param calculator
		 */
		private void redraw(MetroDrawer drawer, Calculator calculator) {
			calculator.revert(min, minCoordF);
			calculator.revert(max, maxCoordF);

			newMinF.set(min);
			newMaxF.set(max);

			bitmap.eraseColor(Color.argb(0, 0, 0, 0));
			if (drawer != null && drawer.isReady()) {
				doDraw(drawer, calculator, canvas);
			} else {
				canvas.drawText("drawer is not yet ready...", 0, size.y - 1,
						paint);
			}
		}

		protected abstract void doDraw(MetroDrawer drawer,
				Calculator calculator, Canvas canvas);

		protected boolean hasChanged() {
			return false;
		}

		private final RectF destRectF = new RectF();

		public void draw(android.graphics.Canvas canvas) {
			if (hasChanged()) {
				forcePendingRedraw();
			}
			destRectF.set(newMinF.x, newMinF.y, newMaxF.x, newMaxF.y);
			// Logger.d(newMinF.x + " " + newMinF.y + " " + newMaxF.x + " "
			// + newMaxF.y);
			canvas.drawBitmap(bitmap, null, destRectF, null);
		}

		public void invalidate() {
			valid = false;
		}

		/**
		 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
		 */
		private class RedrawRunnable implements Runnable {

			Calculator calculator;
			MetroDrawer drawer;

			public void run() {
				redraw(drawer, calculator);
				if (callback != null) {
					callback.onCacheRedrawn();
				}
			}

		}

	}

	/**
	 * @author Eldar Abusalimov
	 */
	private class MainDrawCache extends Cache {

		/**
		 * @param width
		 * @param height
		 */
		public MainDrawCache(int width, int height) {
			super(width, height);
		}

		/**
		 * default constructor.
		 */
		public MainDrawCache() {
			super();
		}

		@Override
		protected void doDraw(MetroDrawer drawer, Calculator calculator,
				Canvas canvas) {
			Logger.d("main cache redraw!");
			evaluateApplyCalculator();
			drawer.draw(canvas);
		}

	}

	/**
	 * @author Eldar Abusalimov
	 */
	private class SelectionCache extends Cache {

		private Station[] stations = new Station[] {};
		private Transition[] transitions = new Transition[] {};

		/**
		 * Constructs new cache with dimension multiplier equaled 2.
		 * 
		 * @param width
		 * @param height
		 */
		public SelectionCache(int width, int height) {
			super(width, height, 2);
		}

		/**
		 * default constructor.
		 */
		public SelectionCache() {
			super();
		}

		@Override
		protected boolean hasChanged() {
			if (stations != selectedStations
					|| transitions != selectedTransitions) {
				stations = selectedStations == null ? new Station[] {}
						: selectedStations;
				transitions = selectedTransitions == null ? new Transition[] {}
						: selectedTransitions;
				return true;
			}
			return super.hasChanged();
		}

		@Override
		protected void doDraw(MetroDrawer drawer, Calculator calculator,
				Canvas canvas) {
			evaluateApplyCalculator();
			drawer.selectStations(canvas, stations);
			drawer.selectTransitions(canvas, transitions);
		}

	}

}
